import MagicString, {DecodedSourceMap} from "magic-string";
import {BinaryExpression, Expression, SpreadElement, StringLiteral, TemplateElement} from "babel-types";
import {ParseResult} from "@babel/core";
import {PathNotVar} from "./types";
import * as Process from "process";
import * as fs from "fs";
import * as path from "path";
import typeOfUtils from "./type-of-utils";
import {Alias, UserConfig} from "vite";
// 遍历
import __traverse__ from 'babel-traverse'

let traverse
const default1 = __traverse__?.default;
if (default1) {
  traverse = default1
} else {
  traverse = __traverse__
}
import * as babel from '@babel/core'
import {Node} from "@babel/traverse";

export default class Utils {
  static getMagicString(code): MagicString {
    return new MagicString(code)
  }

  static getsouceMap(s: MagicString, source: string): DecodedSourceMap {
    return s.generateDecodedMap({
      includeContent: true,
      hires: true,
      source
    })
  }

  static __transCodePathFilesCommon(extensions: string[], returnFunc = false): string {
    return `return (key='')=>{
        const find=Object.keys(value).find(path=>{
          const endPath=path.replace(/\\.\\.\\/|\\.\\//g,'')
          const suffixs=['${extensions.join(`','`)}']
          if(key.endsWith('/')){
            key=key.substring(0,key.length-1)
          }
            if(key.endsWith(endPath)){
            return true
          }
          const find = suffixs
            .find(suffix=>(key+suffix).endsWith(endPath) || (key+'/index'+suffix).endsWith(endPath))
          if(find){
            return true
          }
          return false
        })
        if(find){
          return value[find].default?value[find].default:value[find]
        } else {
          ${returnFunc ? 'return ()=>{}' : 'return {}'}
        }
      }`
  }

  /**
   * 代码转换获取路径下所有文件
   * @param objStr
   * @param extensions
   * @param dyPath
   * @param fenge
   */
  static transCodePathFilesAsync(objStr: string, extensions: string[], dyPath: string, fenge: boolean = false): string {
    let fenggefu = ''
    if (fenge) {
      fenggefu = ';'
    }
    return `${fenggefu}(()=>{
      const value = {${objStr}}
      ${Utils.__transCodePathFilesCommon(extensions, false)}
    })()(${dyPath})`
  }

  static transCodePathFiles(objStr: string, extensions: string[], dyPath: string, fenge: boolean = false): string {
    let fenggefu = ''
    if (fenge) {
      fenggefu = ';'
    }
    return `${fenggefu}(()=>{
       const value = {${objStr}}
      ${Utils.__transCodePathFilesCommon(extensions, true)}
    })()(${dyPath})`
  }

  /**
   * @param left
   */
  static binaryExpressionGeBesttLeft(left: BinaryExpression | StringLiteral | TemplateElement): string {
    if (!left.type) {
      return ''
    }
    if (left.type === 'StringLiteral') {
      return left.value
    }
    if (left.type === 'TemplateElement') {
      return left.value.raw
    }
    return Utils.binaryExpressionGeBesttLeft(left.left as BinaryExpression)
  }

  /**
   * @param right
   */
  static binaryExpressionGeBesttRight(right: BinaryExpression | StringLiteral | TemplateElement): string {
    if (!right.type) {
      return ''
    }
    if (right.type === 'StringLiteral') {
      return right.value
    }
    if (right.type === 'TemplateElement') {
      return right.value.raw
    }
    if (right.type !== "BinaryExpression") {
      return ""
    }
    return Utils.binaryExpressionGeBesttRight(right.right as BinaryExpression)
  }

  static getJsAst(code): ParseResult {
    // @ts-ignore
    return babel.parse(code, {
      sourceType: 'module',
      configFile: false,
    });
  }

  static jsAstTraverse(ast: ParseResult, options?: object) {
    traverse(ast, options)
  }

  static pathNotVar(arg0: Expression | SpreadElement): PathNotVar {
    if (arg0.type === 'StringLiteral') {
      return {notVar: true, value: arg0.value, type: arg0.type}
    }
    if (arg0.type === 'TemplateLiteral') {
      if (arg0.quasis.length === 1) {
        return {notVar: true, value: arg0.quasis[0].value.raw, type: arg0.type}
      }
    }
    return {notVar: false, value: '', type: ''}
  }


  static readDirectory(directory: string, scanSubDirectories: boolean = false, regularExpression: RegExp) {
    const result = {}
    const readDirectory2 = (childDir) => {
      fs.readdirSync(childDir).forEach((file) => {
        let fullPath = path.resolve(childDir, file);
        let isDirectory = false
        if (fs.statSync(fullPath).isDirectory()) {
          isDirectory = true
          if (scanSubDirectories) {
            readDirectory2(fullPath);
          }
        }
        fullPath = Utils.pathChange(fullPath)
        if (!regularExpression.test(fullPath) || isDirectory) return;
        result[fullPath.replace(directory, '.')] = fullPath;
      })
    }
    fs.readdirSync(directory).forEach((file) => {
      let fullPath = path.resolve(directory, file);
      let isDirectory = false
      if (fs.statSync(fullPath).isDirectory()) {
        isDirectory = true
        if (scanSubDirectories) {
          readDirectory2(fullPath);
        }
      }
      fullPath = Utils.pathChange(fullPath)
      if (!regularExpression.test(fullPath) || isDirectory) return;
      result[fullPath.replace(directory, '.')] = fullPath;
    });
    return result
  }

  /**
   *   // for windows, the path.join will return with a path ending with a '/'
   *   // for linux/macos, the path.join will return with a path ending without '/'
   * @param basePath
   */
  static pathChange(basePath: string): string {
    basePath = basePath.replace(/\\/g, '/')
    if (basePath.endsWith('/')) {
      basePath = basePath.substring(0, basePath.length - 1)
    }
    return basePath
  }

  static getBasePath(alias: { [find: string]: string }, workingFilePath: string, directory: string) {
    let basePath: string
    const projectBasePath = Process.cwd()
    switch (directory[0]) {
      // relative path, starting with ./ or ../
      case '.' :
        basePath = path.join(workingFilePath, '..' + path.sep, directory)
        break
      case '/' :
        basePath = path.join(projectBasePath, 'src', directory)
        break
      default:
        const strings = directory.split('/').filter(i => typeOfUtils.isNotEmpty(i));
        const find = Object.keys(alias).find(key => {
          if (key === strings[0]) {
            return true
          }
          return false
        })
        if (find) {
          basePath = path.join(alias[find], directory.substring(find.length))
        } else {
          basePath = path.join(projectBasePath, 'node_modules', directory)
        }
    }
    basePath = Utils.pathChange(basePath)
    return basePath
  }

  /**
   * 获取别名路径
   */
  static getAliasPath(config: UserConfig): { [find: string]: string } {
    const alias = {}
    // @ts-ignore
    if (typeOfUtils.isArray(config.resolve.alias)) {
      // @ts-ignore
      (config.resolve.alias as Array<Alias>).forEach(i => {
        alias[i.find as string] = i.replacement
      })
    } else {
      // @ts-ignore
      Object.keys(config.resolve.alias).forEach(key => {
        // @ts-ignore
        alias[key] = config.resolve.alias[key]
      })
    }
    return alias
  }

  static nodeAddFenge(parent: Node) {
    let fenge = true
    if (['ArrowFunctionExpression', 'CallExpression', 'ObjectProperty', 'ArrayExpression'].includes(parent.type)) {
      fenge = false
    }
    while (parent && fenge) {
      if (parent.type === 'VariableDeclarator') {
        fenge = false
        break;
      }
      // @ts-ignore
      parent = parent.parent
    }
    return fenge
  }
}
